package com.wzw.springboot.principle.use.security.manage;

import com.wzw.springboot.principle.use.cache.redis.RedisService;
import com.wzw.springboot.principle.use.security.constant.Redis;
import com.wzw.springboot.principle.use.security.constant.UserConstant;
import com.wzw.springboot.principle.use.security.infrastructure.config.TokenProperties;
import com.wzw.springboot.principle.use.security.infrastructure.exception.PermitOnlyUserException;
import com.wzw.springboot.principle.use.security.infrastructure.exception.TokenExpireException;
import com.wzw.springboot.principle.use.security.infrastructure.exception.TokenMissingException;
import com.wzw.springboot.principle.use.security.infrastructure.model.SecurityUser;
import io.jsonwebtoken.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

import javax.servlet.http.HttpServletRequest;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * @author Wangzhiwen
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class TokenManager {

    private final RedisService redisService;

    private final TokenProperties tokenProperties;

    public String create(SecurityUser securityUser) {
        securityUser.setUuid(UUID.randomUUID().toString());
        Map<String, Object> claims = getClaims(securityUser);
        String jwtToken = createJwtToken(claims);
        securityUser.setJwtToken(jwtToken);
        refresh(securityUser);
        return jwtToken;
    }

    public void refresh(SecurityUser securityUser) {
        String uuid = securityUser.getUuid();
        long currentTimeMillis = System.currentTimeMillis();
        long expireTimeMills = currentTimeMillis + TimeUnit.HOURS.toMillis(tokenProperties.getExpires());
        securityUser.setLoginTime(currentTimeMillis);
        securityUser.setExpireTime(expireTimeMills);
        redisService.set(Redis.generateTokenKey(securityUser.getId(), uuid), securityUser, Duration.ofMillis(expireTimeMills));
    }

    private Map<String, Object> getClaims(SecurityUser securityUser) {
        Map<String, Object> claims = new HashMap<>(6);
        claims.put(UserConstant.ID, String.valueOf(securityUser.getId()));
        claims.put(UserConstant.USER_NAME, securityUser.getUsername());
        claims.put(UserConstant.MOBILE, securityUser.getMobile());
        claims.put(UserConstant.ROLES, securityUser.getRoles());
        claims.put(UserConstant.UUID, securityUser.getUuid());
        claims.put(UserConstant.PREM, securityUser.getPermissions());
        claims.put(UserConstant.ACCOUNT, securityUser.getAccount());
        return claims;
    }

    private String createJwtToken(Map<String, Object> claims) {

        return Jwts.builder()
                // 签发人
                .setIssuer("Spring-principle")
                // 主题
                .setSubject("Auth")
                // 过期时间
                .setExpiration(new Date(System.currentTimeMillis() + TimeUnit.HOURS.toMillis(tokenProperties.getExpires())))
                // 受众
                .setAudience("All")
                // 生效时间
                .setNotBefore(new Date())
                // 签发时间
                .setIssuedAt(new Date())
                // JWT ID
                .setId(String.valueOf(claims.get(UserConstant.UUID)))
                // 自定义内容
                .setClaims(claims)
                // 签名
                .signWith(SignatureAlgorithm.HS512, tokenProperties.getSecret())
                .compressWith(CompressionCodecs.GZIP)
                .compact();
    }

    public SecurityUser parse(HttpServletRequest request) {
        Assert.notNull(request, "HttpServletRequest Is Null");
        String token = getTokenByRequestHeader(request);
        if (!StringUtils.hasText(token)) {
            token = getTokenByRequestPath(request);
        }
        return parse(token);
    }

    @SuppressWarnings("all")
    public SecurityUser parse(String token) {
        if (Objects.isNull(token) || token.trim().isEmpty()) {
            throw new TokenMissingException();
        }
        Claims body;
        try {
            body = Jwts.parser().setSigningKey(tokenProperties.getSecret()).parseClaimsJws(token).getBody();
        } catch (ExpiredJwtException | UnsupportedJwtException | MalformedJwtException | SignatureException | IllegalArgumentException e) {
            throw new RuntimeException("token parse error");
        }
        if (Objects.isNull(body)) {
            throw new TokenMissingException();
        }
        Long id = Long.parseLong(body.get(UserConstant.ID, String.class));
        String tokenFlag = body.get(UserConstant.UUID, String.class);
        if (!redisService.hasKey(Redis.generateTokenKey(id, tokenFlag))) {
            throw new TokenExpireException();
        }
        SecurityUser securityUser = (SecurityUser) redisService.get(Redis.generateTokenKey(id, tokenFlag));
        // 只允许一人登录
        if (!Objects.equals(tokenFlag, securityUser.getUuid())) {
            throw new PermitOnlyUserException();
        }
        return securityUser;
    }

    public void clean(Long userId) {
        redisService.del(Redis.LOGIN_USER_TOKENS + userId);
    }

    public void clean(HttpServletRequest request) {
        Assert.notNull(request, "HttpServletRequest Is Null");
        SecurityUser securityUser = null;
        try {
            securityUser = parse(request);
        } catch (Exception e) {
            // ignored
        }
        if (Objects.isNull(securityUser)) {
            return;
        }
        clean(securityUser.getId());
    }

    public boolean isExpired(String token) {
        SecurityUser securityUser = parse(token);
        return securityUser.getExpireTime().compareTo(securityUser.getLoginTime()) <= 0;
    }

    public String getTokenByRequestHeader(HttpServletRequest request) {
        return request.getHeader(tokenProperties.getHeader());
    }

    public String getTokenByRequestPath(HttpServletRequest request) {
        return request.getParameter(tokenProperties.getHeader());
    }

    public void renewal(SecurityUser securityUser) {
        if (securityUser.getExpireTime() - securityUser.getLoginTime() < TimeUnit.MILLISECONDS.toMillis(5)) {
            refresh(securityUser);
        }
    }

    public Long authRecord(String account) {
        return redisService.increment(UserConstant.ACCOUNT + ":" + account, 1L);
    }
}
